﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.Practices.CompositeUI.Commands;
using Microsoft.Practices.CompositeUI.EventBroker;
using Microsoft.Practices.CompositeUI.Properties;
using Microsoft.Practices.CompositeUI.Utility;
using Microsoft.Practices.ObjectBuilder;

namespace Microsoft.Practices.CompositeUI.Collections
{
    /// <summary>
    /// 	Represents a filtered collection of objects, based on the locator used by the <see cref = "WorkItem" />.
    /// </summary>
    public class ManagedObjectCollection<TItem> : ICollection, IEnumerable<KeyValuePair<string, TItem>> where TItem : class
    {
        #region Fields & Properties

        private readonly ILifetimeContainer _container;
        private readonly IReadWriteLocator _locator;
        private readonly IBuilder<BuilderStage> _builder;
        private readonly SearchMode _searchMode;
        private readonly Func<Type, string, TItem> _indexerCreation;
        private readonly Predicate<TItem> _filter;

        private readonly WorkItem _workItem;

        #endregion

        #region Collection Fields & Properties

        private readonly ManagedObjectCollection<TItem> _parentCollection;

        #endregion

        #region Constructors

        /// <summary>
        /// 	Initializes an instance of the <see cref = "ManagedObjectCollection{TItem}" /> class.
        /// </summary>
        public ManagedObjectCollection(ILifetimeContainer container, IReadWriteLocator locator, IBuilder<BuilderStage> builder, SearchMode searchMode, Func<Type, string, TItem> indexerCreation, Predicate<TItem> filter) : this(container, locator, builder, searchMode, indexerCreation, filter, null)
        {
        }

        /// <summary>
        /// 	Initializes an instance of the <see cref = "ManagedObjectCollection{TItem}" /> class.
        /// </summary>
        public ManagedObjectCollection(ILifetimeContainer container, IReadWriteLocator locator, IBuilder<BuilderStage> builder, SearchMode searchMode, Func<Type, string, TItem> indexerCreation, Predicate<TItem> filter, ManagedObjectCollection<TItem> parentCollection)
        {
            _container = container;
            _locator = locator;
            _builder = builder;
            _searchMode = searchMode;
            _indexerCreation = indexerCreation;
            _filter = filter;
            _parentCollection = parentCollection;
            _workItem = locator.Get<WorkItem>(new DependencyResolutionLocatorKey(typeof (WorkItem), null));
            if (_workItem != null)
            {
                _workItem.ObjectAdded += WorkItemItemAdded;
                _workItem.ObjectRemoved += WorkItemItemRemoved;
            }
        }

        #endregion

        #region Events

        /// <summary>
        /// 	Event that's fired when an object is added to the collection.
        /// </summary>
        public event EventHandler<DataEventArgs<TItem>> Added;

        /// <summary>
        /// 	Event that's fired when an object is removed from the collection, or disposed when the
        /// 	collection is cleaning up.
        /// </summary>
        public event EventHandler<DataEventArgs<TItem>> Removed;

        #region Event Handlers

        private void WorkItemItemAdded(object sender, DataEventArgs<object> e)
        {
            var added = Added;
            if (added != null && e.Data is TItem)
            {
                var value = (TItem) e.Data;
                var key = FindObjectInLocator(value);
                if (key != null && key.ID != null && (_filter == null || _filter(value)))
                {
                    added(this, new DataEventArgs<TItem>(value));
                }
            }
        }

        private void WorkItemItemRemoved(object sender, DataEventArgs<object> e)
        {
            var removed = Removed;
            if (removed != null && e.Data is TItem)
            {
                var value = (TItem) e.Data;
                var key = FindObjectInLocator(value);
                if (key != null && key.ID != null && (_filter == null || _filter(value)))
                {
                    removed(this, new DataEventArgs<TItem>(value));
                }
            }
        }

        private DependencyResolutionLocatorKey FindObjectInLocator(object value)
        {
            return _locator.Where(pair => pair.Key is DependencyResolutionLocatorKey && pair.Value == value).Select(pair => ((DependencyResolutionLocatorKey) pair.Key)).FirstOrDefault();
        }

        #endregion

        #endregion

        #region Implementation of IEnumerable

        /// <summary>
        /// 	Returns an enumerator that iterates through the collection.
        /// </summary>
        /// <returns>
        /// 	A <see cref = "T:System.Collections.Generic.IEnumerator`1" /> that can be used to iterate through the collection.
        /// </returns>
        /// <filterpriority>1</filterpriority>
        public IEnumerator<KeyValuePair<string, TItem>> GetEnumerator()
        {
            var seenNames = new Dictionary<string, object>();
            IReadableLocator currentLocator = _locator;
            do
            {
                foreach (var pair in currentLocator.Where(p => p.Key is DependencyResolutionLocatorKey))
                {
                    var key = (DependencyResolutionLocatorKey) pair.Key;
                    if (key.ID != null && pair.Value is TItem)
                    {
                        var id = key.ID;
                        var value = (TItem) pair.Value;
                        if (!seenNames.ContainsKey(id) && (_filter == null || _filter(value)))
                        {
                            seenNames[id] = String.Empty;
                            yield return new KeyValuePair<string, TItem>(id, value);
                        }
                    }
                }
            }
            while (_searchMode == SearchMode.Up && (currentLocator = currentLocator.ParentLocator) != null);
        }

        /// <summary>
        /// 	Returns an enumerator that iterates through a collection.
        /// </summary>
        /// <returns>
        /// 	An <see cref = "T:System.Collections.IEnumerator" /> object that can be used to iterate through the collection.
        /// </returns>
        /// <filterpriority>2</filterpriority>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion

        #region Implementation of ICollection

        /// <summary>
        /// 	Copies the elements of the <see cref = "T:System.Collections.ICollection" /> to an <see cref = "T:System.Array" />, starting at a particular <see
        ///  	cref = "T:System.Array" /> index.
        /// </summary>
        /// <param name = "array">The one-dimensional <see cref = "T:System.Array" /> that is the destination of the elements copied from <see
        ///  	cref = "T:System.Collections.ICollection" />. The <see cref = "T:System.Array" /> must have zero-based indexing. </param>
        /// <param name = "index">The zero-based index in <paramref name = "array" /> at which copying begins. </param>
        /// <exception cref = "T:System.ArgumentNullException"><paramref name = "array" /> is null. </exception>
        /// <exception cref = "T:System.ArgumentOutOfRangeException"><paramref name = "index" /> is less than zero. </exception>
        /// <exception cref = "T:System.ArgumentException"><paramref name = "array" /> is multidimensional.-or- The number of elements in the source <see
        ///  	cref = "T:System.Collections.ICollection" /> is greater than the available space from <paramref name = "index" /> to the end of the destination <paramref
        ///  	name = "array" />. </exception>
        /// <exception cref = "T:System.ArgumentException">The type of the source <see cref = "T:System.Collections.ICollection" /> cannot be cast automatically to the type of the destination <paramref
        ///  	name = "array" />. </exception>
        /// <filterpriority>2</filterpriority>
        void ICollection.CopyTo(Array array, int index)
        {
            this.ToArray().CopyTo(array, index);
        }

        /// <summary>
        /// 	Gets the number of elements contained in the <see cref = "T:System.Collections.ICollection" />.
        /// </summary>
        /// <returns>
        /// 	The number of elements contained in the <see cref = "T:System.Collections.ICollection" />.
        /// </returns>
        /// <filterpriority>2</filterpriority>
        public int Count
        {
            get
            {
                var count = _locator.Count(pair => pair.Key is DependencyResolutionLocatorKey && pair.Key != null && ((DependencyResolutionLocatorKey) pair.Key).ID != null && pair.Value is TItem && (_filter == null || _filter((TItem) pair.Value)));
                if (_searchMode == SearchMode.Up && _parentCollection != null)
                {
                    count += _parentCollection.Count;
                }
                return count;
            }
        }

        /// <summary>
        /// 	Gets an object that can be used to synchronize access to the <see cref = "T:System.Collections.ICollection" />.
        /// </summary>
        /// <returns>
        /// 	An object that can be used to synchronize access to the <see cref = "T:System.Collections.ICollection" />.
        /// </returns>
        /// <filterpriority>2</filterpriority>
        object ICollection.SyncRoot
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        /// <summary>
        /// 	Gets a value indicating whether access to the <see cref = "T:System.Collections.ICollection" /> is synchronized (thread safe).
        /// </summary>
        /// <returns>
        /// 	true if access to the <see cref = "T:System.Collections.ICollection" /> is synchronized (thread safe); otherwise, false.
        /// </returns>
        /// <filterpriority>2</filterpriority>
        bool ICollection.IsSynchronized
        {
            get
            {
                return false;
            }
        }

        #endregion

        #region Indexer

        /// <summary>
        /// 	Gets an item by ID.
        /// </summary>
        /// <param name = "id">The ID of the item to get.</param>
        /// <returns>The item, if present; null otherwise. If the collection has been given an IndexerCreation and the item is not found, it will be created using the delegate.
        /// </returns>
        public TItem this[string id]
        {
            get
            {
                var item = Get(id);
                if (item == null && _indexerCreation != null && !Contains(id, _searchMode, false))
                {
                    item = _indexerCreation(typeof (TItem), id);
                    Add(item, id);
                }
                return item;
            }
        }

        #endregion

        #region Add

        /// <summary>
        /// 	Adds an item to the collection, without an ID.
        /// </summary>
        /// <param name = "item">The item to be added.</param>
        public void Add(TItem item)
        {
            Add(item, null);
        }

        /// <summary>
        /// 	Adds an item to the collection, with an ID.
        /// </summary>
        /// <param name = "item">The item to be added.</param>
        /// <param name = "id">The ID of the item.</param>
        public void Add(TItem item, string id)
        {
            Guard.ArgumentNotNull(item, "item");
            Build(item.GetType(), id, item);
        }

        /// <summary>
        /// 	Creates a new item and adds it to the collection, without an ID.
        /// </summary>
        /// <param name = "typeToBuild">The type of item to be built.</param>
        /// <returns>The newly created item.</returns>
        public TItem AddNew(Type typeToBuild)
        {
            return AddNew(typeToBuild, null);
        }

        /// <summary>
        /// 	Creates a new item and adds it to the collection, with an ID.
        /// </summary>
        /// <param name = "typeToBuild">The type of item to be built.</param>
        /// <param name = "id">The ID of the item.</param>
        /// <returns>The newly created item.</returns>
        public TItem AddNew(Type typeToBuild, string id)
        {
            return Build(typeToBuild, id, null);
        }

        /// <summary>
        /// 	Creates a new item and adds it to the collection, without an ID.
        /// </summary>
        /// <typeparam name = "TTypeToBuild">The type of item to be built.</typeparam>
        /// <returns>The newly created item.</returns>
        public TTypeToBuild AddNew<TTypeToBuild>() where TTypeToBuild : TItem
        {
            return (TTypeToBuild) AddNew(typeof (TTypeToBuild), null);
        }

        /// <summary>
        /// 	Creates a new item and adds it to the collection, with an ID.
        /// </summary>
        /// <typeparam name = "TTypeToBuild">The type of item to be built.</typeparam>
        /// <param name = "id">The ID of the item.</param>
        /// <returns>The newly created item.</returns>
        public TTypeToBuild AddNew<TTypeToBuild>(string id) where TTypeToBuild : TItem
        {
            return (TTypeToBuild) AddNew(typeof (TTypeToBuild), id);
        }

        #endregion

        #region Build

        private TItem Build(Type typeToBuild, string idToBuild, object item)
        {
            if (idToBuild != null && Contains(idToBuild, SearchMode.Local, true))
            {
                throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, Resources.DuplicateID, idToBuild));
            }
            if (item != null && ReferenceEquals(item, _locator.Get(new DependencyResolutionLocatorKey(typeof (WorkItem), null))))
            {
                throw new ArgumentException(Resources.CannotAddWorkItemToItself, "item");
            }
            if (item == null)
            {
                item = BuildFirstTimeItem(typeToBuild, idToBuild, null);
            }
            else if (!_container.Contains(item))
            {
                item = BuildFirstTimeItem(typeToBuild, idToBuild, item);
            }
            else
            {
                BuildRepeatedItem(typeToBuild, idToBuild, item);
            }
            return (TItem) item;
        }

        /// <summary>
        /// 	Used to build a first time item (either an existing one or a new one). The Builder will
        /// 	end up locating it, and we add it to the container.
        /// </summary>
        private object BuildFirstTimeItem(Type typeToBuild, string idToBuild, object item)
        {
            return _builder.BuildUp(_locator, typeToBuild, idToBuild ?? Guid.NewGuid().ToString(), item);
        }

        /// <summary>
        /// 	Used to "build" an item we've already seen once. We don't use the builder, because that
        /// 	would do double-injection. Since it's already in the lifetime container, all we need to
        /// 	do is add a second locator registration for it for the right name.
        /// </summary>
        private void BuildRepeatedItem(Type typeToBuild, string idToBuild, object item)
        {
            _locator.Add(new DependencyResolutionLocatorKey(typeToBuild, idToBuild ?? Guid.NewGuid().ToString()), item);
        }

        #endregion

        #region Get

        /// <summary>
        /// 	Gets the object with the given ID.
        /// </summary>
        /// <param name = "id">The ID to get.</param>
        /// <returns>The object, if present; null otherwise.</returns>
        public TItem Get(string id)
        {
            return Get(id, _searchMode, true);
        }

        private TItem Get(string id, SearchMode searchMode, bool filtered)
        {
            Guard.ArgumentNotNull(id, "id");
            foreach (var pair in _locator.Where(p => p.Key is DependencyResolutionLocatorKey))
            {
                var key = (DependencyResolutionLocatorKey) pair.Key;
                if (Equals(key.ID, id))
                {
                    var item = (TItem) pair.Value;
                    if (!filtered || _filter == null || _filter(item))
                    {
                        return item;
                    }
                }
            }
            if (searchMode == SearchMode.Up && _parentCollection != null)
            {
                return _parentCollection.Get(id);
            }
            return default(TItem);
        }

        /// <summary>
        /// 	Gets the object with the given ID.
        /// </summary>
        /// <typeparam name = "TTypeToGet">The type of the object to get.</typeparam>
        /// <param name = "id">The ID to get.</param>
        /// <returns>The object, if present; null otherwise.</returns>
        public TTypeToGet Get<TTypeToGet>(string id) where TTypeToGet : TItem
        {
            return (TTypeToGet) Get(id);
        }

        #endregion

        #region Contains

        /// <summary>
        /// 	Determines if the collection contains an object with the given ID.
        /// </summary>
        /// <param name = "id">The ID of the object.</param>
        /// <returns>Returns true if the collection contains the object; false otherwise.</returns>
        public bool Contains(string id)
        {
            return Get(id) != null;
        }

        private bool Contains(string id, SearchMode searchMode, bool filtered)
        {
            return Get(id, searchMode, filtered) != null;
        }

        /// <summary>
        /// 	Determines if the collection contains an object.
        /// </summary>
        /// <param name = "item">The object.</param>
        /// <returns>Returns true if the collection contains the object; false otherwise.</returns>
        public bool ContainsObject(TItem item)
        {
            return _container.Contains(item);
        }

        #endregion

        #region Find By Type

        /// <summary>
        /// 	Finds all objects that are type-compatible with the given type.
        /// </summary>
        /// <typeparam name = "TSearchType">The type of item to find.</typeparam>
        /// <returns>A collection of the found items.</returns>
        public ICollection<TSearchType> FindByType<TSearchType>() where TSearchType : TItem
        {
            var result = _container.Where(obj => typeof (TSearchType).IsAssignableFrom(obj.GetType())).Cast<TSearchType>().ToList();
            if (_searchMode == SearchMode.Up && _parentCollection != null)
            {
                result.AddRange(_parentCollection.FindByType<TSearchType>());
            }
            return result;
        }

        /// <summary>
        /// 	Finds all objects that are type-compatible with the given type.
        /// </summary>
        /// <param name = "searchType">The type of item to find.</param>
        /// <returns>A collection of the found items.</returns>
        public ICollection<TItem> FindByType(Type searchType)
        {
            var result = _container.Where(obj => searchType.IsAssignableFrom(obj.GetType())).Cast<TItem>().ToList();
            if (_searchMode == SearchMode.Up && _parentCollection != null)
            {
                result.AddRange(_parentCollection.FindByType(searchType));
            }
            return result;
        }

        #endregion

        #region Remove

        /// <summary>
        /// 	Removes an object from the container.
        /// </summary>
        /// <param name = "item">The item to be removed.</param>
        public void Remove(TItem item)
        {
            if (_container.Contains(item))
            {
                if (item is WorkItem)
                {
                    throw new ArgumentException(Resources.NoRemoveWorkItemFromManagedObjectCollection, "item");
                }
                if (item is Command)
                {
                    var command = item as Command;
                    throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, Resources.RemoveCommandFromWorkItemIsNotPermitted, command.Name), "item");
                }
                if (item is EventTopic)
                {
                    var eventTopic = item as EventTopic;
                    throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, Resources.RemoveEventTopicFromWorkItemIsNotPermitted, eventTopic.Name), "item");
                }
                _builder.TearDown(_locator, item);
                _container.Remove(item);
                var keysToRemove = _locator.Where(pair => pair.Value.Equals(item)).Select(pair => pair.Key).ToList();
                foreach (var key in keysToRemove)
                {
                    _locator.Remove(key);
                }
            }
        }

        #endregion
    }
}